﻿<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Processing.js Ref Tests</title>
    <script src="/processing.min.js"></script>
    <script src="tests.js"></script>
    <style>
      .test  { border: solid 1px; margin: 5px; }
      .title { margin-left: 5px; }
    </style>
  </head>
  <body>
    <h1>Processing.js Ref Test Runner</h1>
    <p>To start the tests click Start.  Each test is run in order (downloaded from the server).  The first canvas is the reference image, the second is a live sketch that has just been executed, the third is a visual diff.  The diff is calculated with a tolerance value (e.g., ~5%).  If the third canvas is green, the test passed.  If it is white with red pixels (or all red), it failed, and the red shows where the pixels differ. The calibrating must fail on some tests.  You can generate ref tests <a href="ref-test-builder.html">here</a>.</p>

    <p>Performance tests can be run from <a href="/perf" target="_blank">here.</a>

    <h3>Settings</h3>
    <div>Run Tests:
      <select id="test-type" onchange="updateSelectedTests();">
        <option value="2D,3D" selected>2D and 3D</option>
        <option value="2D">2D Only</option>
        <option value="3D">3D Only</option>
        <option value="BLEND">Blend Only</option>
        <option value="Color Profile">Color Profile</option>
        <option value="endShape2D">endShape2D</option>
        <option value="endShape3D">endShape3D</option>
        <option value="Calibration">Calibration</option>
        <option value="Convolution">Convolution</option>
        <option value="SVG">SVG</option>
        <option value="Text">Text</option>
        <option value="Crisp">Crisp</option>
        <option value="Test Suite">Test Suite</option>
      </select>
      Blur Radius: <input type="text" size="2" id="sigma" onchange="updateTolerance();">
      Epsilon (0-1.0): <input type="text" size="4" id="epsilon" onchange="updateTolerance();">
      Remove Passed Tests: <input id="remove-passed" type="checkbox">
      Stop on First Fail: <input id="stop-on-fail" type="checkbox">
      <input id="testStart" onclick="runTests(selectedTests);" type="button" value="Start">
      <input id="testStop" onclick="endTests();" type="button" value="Stop" style="display:none">
      <span id="testCount"></span>
    </div>

    <h3>Results</h3>
    <div id="status" style="margin-bottom: 10px;"></div>
    <div id="results" style="margin: 5px; padding-top: 10px;"></div>
    <div id="total"></div>

    <script type="text/javascript">
      /**
       *
       */
      var isWebGLBrowser = (function() {
        var canvas = document.createElement('canvas'),
          contextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"],
          i = contextNames.length,
          ctx;

        while (i--) {
          try {
            ctx = canvas.getContext(contextNames[i]);
            if (ctx) {
              return true;
            }
          } catch(e) {}
        }
        return false;
      })();

      var selectedTests = [];
      var total = document.getElementById('total');
      total.innerHTML = '';
      var removePassed;
      var stopTests;

      updateSelectedTests();

      // Tolerance values and kernel for blur
      var sigma = 2; // radius
      document.getElementById('sigma').value = sigma;
      var epsilon = 0.05; // match accuracy ~5%
      document.getElementById('epsilon').value = epsilon;
      var kernel, kernelSize;
      buildKernel();

      /**
       *
       */
      function updateSelectedTests() {
        var selectControl = document.getElementById('test-type');
        var selectedTags = selectControl.value.split(',');

        selectedTests = new Array();
        for(var i = 0; i < tests.length; ++i) {
          var found = false;
          // checking if any selected tags present in test tags
          for(var j = 0; j < selectedTags.length && !found; ++j) {
            for(var q = 0; q < tests[i].tags.length && !found; ++q) {
              if(tests[i].tags[q] == selectedTags[j]) found = true;
            }
          }
          if(found) {
            selectedTests.push(tests[i]);
          }
        }

        var testCount = document.getElementById('testCount');
        testCount.innerHTML = '&nbsp;(' + selectedTests.length + ' tests)';
      }

      /**
       *
       */
      function buildKernel() {
        var ss = sigma * sigma;
        var factor = 2 * Math.PI * ss;
        kernel = new Array();
        kernel.push(new Array());
        var i = 0, j;
        do {
            var g = Math.exp(-(i * i) / (2 * ss)) / factor;
            if (g < 1e-3) break;
            kernel[0].push(g);
            ++i;
        } while (i < 7);
        kernelSize = i;
        for (j = 1; j < kernelSize; ++j) {
            kernel.push(new Array());
            for (i = 0; i < kernelSize; ++i) {
                var g = Math.exp(-(i * i + j * j) / (2 * ss)) / factor;
                kernel[j].push(g);
            }
        }
      }

      /**
       *
       */
      function updateTolerance() {
        var newS = document.getElementById('sigma').value;
        if (newS)
          sigma = parseInt(newS, 10);

        buildKernel();

        var newE = document.getElementById('epsilon').value;
        if (newE)
          epsilon = parseFloat(newE);
      }

      /**
       *
       */
      function download(file) {
        var req = new XMLHttpRequest();
        req.open('GET', file, false);
        if (req.overrideMimeType) {
          req.overrideMimeType('text/plain');
        }
        req.send(null);
        if (req.status != 200 && req.status !=0) {
          return null;
        } else {
          return req.responseText;
        }
      }


      /**
       *
       */
      var passedCount = 0,
          failedCount = 0,
          knownFailedCount = 0;

      /**
       *
       */
      function getTest(testName) {
        var responseText = download(testName);

        function is3D(script) {
          // look for size(100,100,OPENGL) or size(100,100,P3D)
          var match = script.match(/size\(\s*\d+\s*\,\s*\d+\s*\,\s*(\w+)\s*\);/);
          if (match && match[1]) {
            return match[1] == "OPENGL" || match[1] == "P3D";
          }
          return false;
        }

        if (!responseText) {
          return null;
        } else {
          // Split out the canvas info in the comment:
          var test = {name: testName, code: responseText};
          // looking for: //[100,100]0,0,67,0,34,...\n
          var m = /^\/\/\[([^\]]+)\]([^\n]+)\n/.exec(test.code);
          if (m && m.length == 3) {
            var dims = m[1].split(',');
            test.width = parseInt(dims[0], 10);
            test.height = parseInt(dims[1], 10);
            test.pixels = new Array(test.width * test.height * 4);
            var pixelStr = m[2].split(',');
            for (var i=0, l=pixelStr.length; i<l; i++) {
              test.pixels[i] = parseInt(pixelStr[i], 10);
            }
            test.is3D = is3D(test.code);
            return test;
          } else {
            // Malformed test
            return null;
          }
        }
      }

      /**
       *
       */
      function endTests(){
        document.getElementById('testStart').style.display="inline";
        document.getElementById('testStop').style.display="none";
        stopTests = true;
        // call-and-forget doesn't need an XMLHTTPRequest. Just an image.
        var img = new Image(),
            url = "/stopserver/"+failedCount+"/"+knownFailedCount+"/"+passedCount;
        console.log("stopping server through " + url);
        img.src = url;
      }

      /**
       *
       */
      function runTests(tests) {
        var tl = tests.length;
        passedCount = 0;
        failedCount = 0;
        knownFailedCount = 0;
        stopTests = false;
        document.getElementById('testStart').style.display="none";
        document.getElementById('testStop').style.display="inline";
        var results = document.getElementById('results');
        results.innerHTML = '';
        var total = document.getElementById('total');
        total.innerHTML = '';

        removePassed = document.getElementById('remove-passed').checked;

        /**
         *
         */
        var buildCanvas = function(id, w, h) {
          var c = document.createElement('canvas');
          c.id = id;
          c.width = w;
          c.height = h;
          c.className = "test";
          return c;
        };

        /**
         *
         */
        var link = function(name) {
          return '<a href="' + name + '">' + name + '</a>';
        };

        /**
         *
         */
        var titleText = function(testNumber, testTotal, time, testName, hasFailed, message) {
          return "Test (" + testNumber + "/" + testTotal + ") [" + time + "ms]: " + link(testName) +
                 (hasFailed ? " -- FAILED (" + message + ")" : message ? " -- PASSED (" + message + ")" : " -- PASSED ");
        };

        /**
         *
         */
        var getPixels = function(aCanvas, isWebGL) {
          try {
            if (isWebGL) {
              var context = aCanvas.getContext("experimental-webgl");

              var data = null;
              try{
                // try deprecated way first
                data = context.readPixels(0, 0, aCanvas.width, aCanvas.height, context.RGBA, context.UNSIGNED_BYTE);

                // Chrome posts an error
                if(context.getError()){
                  throw new Error("readPixels() API has changed.");
                }
              }catch(e){
                // if that failed, try new way
                if(!data){
                  data = new Uint8Array(aCanvas.width * aCanvas.height * 4);
                  context.readPixels(0, 0, aCanvas.width, aCanvas.height, context.RGBA, context.UNSIGNED_BYTE, data);
                }
              }
              return data;
            } else {
              return aCanvas.getContext('2d').getImageData(0, 0, aCanvas.width, aCanvas.height).data;
            }
          } catch (e) {
            return null;
          }
        };

        /**
         *
         */
        function nextTest(testNum) {
          if (document.getElementById('stop-on-fail').checked && failedCount){
            endTests();
          }

          if (!stopTests && testNum < tl) {
            window.setTimeout(function() { runOne(testNum); }, 10);
          } else {
            endTests();
            var info = "Tests Completed - " + failedCount + " failed, " + knownFailedCount + " known failures, " + passedCount + " passed, " + (failedCount + passedCount + knownFailedCount) + " total.";
            document.getElementById('status').innerHTML = info;
            total.innerHTML = info;

            // sort the tests into failed, known failures, and passed
            var failedDiv = document.createElement('div');
            failedDiv.innerHTML = '<hr><h3>Failed ' + failedCount + ' of ' + (passedCount + failedCount + knownFailedCount) + '</h3>';

            var knownFailureDiv = document.createElement('div');
            knownFailureDiv.innerHTML = '<hr><h3>Known Failures ' + knownFailedCount + ' of ' + (passedCount + failedCount + knownFailedCount) + '</h3>';

            var passDiv = document.createElement('div');
            passDiv.innerHTML = '<hr><h3>Passed ' + passedCount + ' of ' + (passedCount + failedCount + knownFailedCount) + '</h3>';

            while (results.hasChildNodes()) {
              var childNode = results.removeChild(results.firstChild);
              if (childNode.className === 'failed') {
                failedDiv.appendChild(childNode);
              } else if (childNode.className === 'knownfailure') {
                knownFailureDiv.appendChild(childNode);
              } else {
                passDiv.appendChild(childNode);
              }
            }

            if (failedCount < 1) {
              var temp = document.createElement('p');
              temp.innerHTML = 'No failed tests';
              failedDiv.appendChild(temp);
            } else if (knownFailedCount < 1) {
              var temp = document.createElement('p');
              temp.innerHTML = 'No known failures';
              knownFailureDiv.appendChild(temp);
            } else if (passedCount < 1) {
              var temp = document.createElement('p');
              temp.innerHTML = 'No passed tests';
              passDiv.appendChild(temp);
            }

            results.appendChild(failedDiv);
            results.appendChild(knownFailureDiv);
            results.appendChild(passDiv);
          }
        }

        /**
         *
         */
        var runOne = function(i) {
          var status = document.getElementById('status');
          status.innerHTML = "Running Test " + i + "/" + (tests.length-1);

          var test = getTest(tests[i].path);
          test.knownFailureTicket = tests[i].knownFailureTicket;
          test.epsilonOverride = tests[i].epsilonOverride
          var result = document.createElement('div');
          result.id = i
          var title = document.createElement('div');
          title.className = "title";
          result.appendChild(title);
          results.appendChild(result);
          var valueEpsilon = epsilon * 255;
          if (test.epsilonOverride && test.epsilonOverride > epsilon) {
            valueEpsilon = test.epsilonOverride * 255;
          }

          if (test) {
            var original = buildCanvas(test.name + '-original', test.width, test.height);
            var current  = buildCanvas(test.name + '-current', test.width, test.height);
            var diff     = buildCanvas(test.name + '-diff', test.width, test.height);

            result.appendChild(original);
            result.appendChild(current);
            result.appendChild(diff);

            var pixelsLen = test.pixels.length;

            function validateResult() {
                var is3D = test.is3D;
                totalTime = Date.now() - startTime;

                // draw the original based on stored pixels
                var origCtx = original.getContext('2d');
                var origData = origCtx.createImageData(original.width, original.height);
                for (var l=0; l < pixelsLen; l++) {
                  if (is3D) {
                    // WebGL inverts the rows in readPixels vs. the 2D context. Flip the image around so it looks right
                    origData.data[l] = test.pixels[(original.height - 1 - Math.floor(l / 4 / original.width)) *
                                                    original.width * 4 + (l % (original.width * 4))];
                  } else {
                    origData.data[l] = test.pixels[l];
                  }
                }
                origCtx.putImageData(origData, 0, 0);

                // Blur pixels for diff
                test.pixels = blur(test.pixels, test.width, test.height);

                // do a visual diff on the pixels
                var currData = getPixels(current, is3D);
                if (!currData) {
                  title.innerHTML = titleText(i+1, tl, totalTime, test.name, true, "can't diff pixels");
                  result.className = "failed";
                  failedCount++;
                  return nextTest(i+1);
                }

                if (currData.length == test.pixels.length) {
                  currData = blur(currData, test.width, test.height);
                  var diffCtx = diff.getContext('2d');
                  var diffData = diffCtx.createImageData(current.width, current.height);
                  var tp = test.pixels;
                  var failed = false;
                  for (var j=0; j < pixelsLen;) {
                    if (Math.abs(currData[j] - tp[j]) < valueEpsilon  &&
                        Math.abs(currData[j + 1] - tp[j + 1]) < valueEpsilon &&
                        Math.abs(currData[j + 2] - tp[j + 2]) < valueEpsilon &&
                        Math.abs(currData[j + 3] - tp[j + 3]) < valueEpsilon)  {
                      diffData.data[j] = diffData.data[j+1] = diffData.data[j+2] = diffData.data[j+3] = 0;
                    } else {
                      diffData.data[j] = 255;
                      diffData.data[j+1] = diffData.data[j+2] = 0;
                      diffData.data[j+3] = 255;
                      failed = true;
                    }
                    j+=4;
                  }
                  if (failed && is3D) {
                    var w = 4 * test.width, offset1 = 0, offset2 = diffData.data.length - w;
                    while(offset1 < offset2) {
                      for(var col=0; col<w; col++) {
                        var tmp = diffData.data[offset1];
                        diffData.data[offset1] = diffData.data[offset2];
                        diffData.data[offset2] = tmp;
                        offset1++;
                        offset2++;
                      }
                      offset2 -= w + w;
                    }
                  }
                  if (test.knownFailureTicket) {
                    diffCtx.putImageData(diffData, 0, 0);
                    title.innerHTML = titleText(i+1, tl, totalTime, test.name, true, "Known failure. See <a href='https://processing-js.lighthouseapp.com/projects/41284/tickets/" + test.knownFailureTicket + "'>ticket #" + test.knownFailureTicket + "</a>");
                    result.className = "knownfailure";
                    knownFailedCount++;
                  } else if (failed) {
                    diffCtx.putImageData(diffData, 0, 0);
                    title.innerHTML = titleText(i+1, tl, totalTime, test.name, true, "pixels off");
                    result.className = "failed";
                    failedCount++;
                  } else {
                    diffCtx.fillStyle = "rgb(0,255,0)";
                    diffCtx.fillRect (0, 0, diff.width, diff.height);
                    if (test.epsilonOverride && test.epsilonOverride > epsilon) {
                      title.innerHTML = titleText(i+1, tl, totalTime, test.name, false, "epsilonOverride = " + test.epsilonOverride);
                      result.className = "passed";
                    } else {
                      title.innerHTML = titleText(i+1, tl, totalTime, test.name, false, "");
                      result.className = "passed";
                    }
                    passedCount++;
                    if (removePassed) {
                      result.parentNode.removeChild(result);
                      result = null;
                    }
                  }
                } else {
                  title.innerHTML = titleText(i+1, tl, totalTime, test.name, true, "size mismatch");
                  result.className = "failed";
                  failedCount++;
                }
                nextTest(i+1);
            }

            if (test.is3D && !isWebGLBrowser) {
              title.innerHTML = titleText(i+1, tl, 0, test.name, true, "Processing failed: WebGL context is not supported on this browser.");
              result.className = "failed";
              failedCount++;
              return nextTest(i+1);
            } else {
              // draw the current version from code, timing it
              var startTime = Date.now(), totalTime = 0;
              var p, s;
              try {
                s = Processing.compile(test.code);
                s.onExit = validateResult;
                p = new Processing(current, s);
              } catch (e) {
                if (test.knownFailureTicket) {
                  title.innerHTML = titleText(i+1, tl, 0, test.name, true, "Processing failed: " + e.toString() + ". Known failure. See <a href='https://processing-js.lighthouseapp.com/projects/41284/tickets/" + test.knownFailureTicket + "'>ticket #" + test.knownFailureTicket + "</a>");
                  result.className = "knownfailure";
                  knownFailedCount++;
                } else {
                  title.innerHTML = titleText(i+1, tl, 0, test.name, true, "Processing failed: " + e.toString());
                  result.className = "failed";
                  failedCount++;
                }
                return nextTest(i+1);
              }
            }
          } else {
            title.innerHTML = titleText(i+1, tl, totalTime, test.name, true, "invalid test");
            result.className = "failed";
            failedCount++;
            nextTest(i+1);
          }
        };
        window.setTimeout(function() { runOne(0); }, 10);
      }

      /**
       *
       */
      function blur(data, width, height) {
        var len = data.length;
        var newData = new Array(len);

        for (var y = 0; y < height; ++y) {
          for (var x = 0; x < width; ++x) {
            var r = 0, g = 0, b = 0, a = 0, sum = 0;
            var j = Math.max(1 - kernelSize, -y), jabs = -j;
            for (; j < kernelSize; jabs = Math.abs(++j)) {
              if(y + j >= height) { break; }
              var i = Math.max(1 - kernelSize, -x), iabs = -i;
              var offset = 4 * ((y + j) * width + (x + i));
              for (; i < kernelSize; iabs = Math.abs(++i)) {
                if (x + i >= width) { break; }
                var k = kernel[jabs][iabs];
                r += data[offset++] * k;
                g += data[offset++] * k;
                b += data[offset++] * k;
                a += data[offset++] * k;
                sum += k;
              }
            }
            var destOffset = 4 * (y * width + x);
            newData[destOffset++] = r / sum;
            newData[destOffset++] = g / sum;
            newData[destOffset++] = b / sum;
            newData[destOffset++] = a / sum;
          }
        }

        return newData;
      }
    </script>

    <!-- autorun -->
    <script>
      document.addEventListener("DOMContentLoaded", function() {
        var loc = window.location.toString();
        if(loc.indexOf("autorun=true") !== -1) {
          runTests(selectedTests);
        }
      }, false);
    </script>

  </body>
</html>
